supce's blog

CSS Secret 读书笔记之缓动效果


弹跳动画

在现实世界里,物体的移动往往不是匀速的。因此,给动画和过渡加上缓动效果会使界面显得更加真实。
回弹是一种常用的缓动效果,对回弹的过程进行分析会发现,回弹就是当一个过渡达到最终值时,往回倒一点,然后再次回到最终值,如此往复一次或者多次,并逐渐收敛。最终稳定在终值。
回弹往往会用在有尺寸变化的元素上,比如对于页面弹出框,先放大再缩小。下面就利用回弹模拟一个下落的弹性小球。

HTML:

<div class="ball_bg">
    <div class="ball ball-a"></div>
</div>

CSS:

.ball_bg{
    width: 30px;
    height: 330px;
    background: skyblue;
    background: linear-gradient(skyblue,white 320px,yellowgreen 0);
}
.ball{
    width: 20px;
    height: 20px;
    border-radius: 50%;
    margin: 0 auto;
    background: red;  /*平稳回退*/
    background: radial-gradient(at 30% 30%,#fdd,red);   
}
@keyframes bounce-a{
    60%,80%,to {
        transform: translateY(300px);
    }
    70% {transform: translateY(200px);}
    90% {transform: translateY(250px);}
}
.ball-a{
    animation: bounce-a 3s forwards;
}

迫不及待地刷新页面:


但是实际的效果令人很失望,小球的移动显得很不真实。这是由于默认的调速函数时这样的:

还有四种内置的缓动曲线如下图所示:

如上图,ease-in和ease-out是相反的版本。这对组合正好满足回弹效果:小球的运动相反时,调速函数也是相反的。于是,修改上面的代码:

.ball-a{
    /*animation: bounce-a 3s forwards;*/
    animation: bounce-a 3s ease-in forwards;
}
@keyframes bounce-a{
    60%,80%,to {
        transform: translateY(300px);
        animation-timing-function: ease-out;
    }
    70% {transform: translateY(200px);}
    90% {transform: translateY(250px);}
}

效果会真是很多:

上面提到的五种曲线都是通过三次贝塞尔曲线来指定的。这种曲线由一定数量的路径片段组成,每个片段的每一端由一个锚点来控制曲率。在CSS的调速函数中,都是只有一个片段的贝塞尔曲线,因此调速函数只有两个控制曲率的锚点。

为了对预定义的曲线进行补充,CSS提供了cubic-bezier()函数,允许开发者指定自定义的调速函数。该函数接收四个参数,分别表示两个控制锚点的坐标值。
比如:cubic-bezier(x1,y1,x2,y2),则(x1,y1)表示第一个锚点坐标,(x2,y2)表示第二个锚点坐标。曲线片段的两端分别固定在(0,0)和(1,1),因此参数的取值范围为[0-1]

我们也可以利用chrome或者图形化工具来设置这四个参数使过渡更加逼真。


弹性过渡

弹性过渡通常用在表单登录的提示框上,下面模拟一个文本输入框,每当它被聚焦时,都需要展示一个提示框。这个提示框用来向用户提示格式等信息。
首先是HTML:

<label>password:</label>
<div class="pwd">
    <input type="text" value="your password" class="uname-b">
    <span class="callout-b callout">
    Only letters, numbers, underscores (_) and hyphens (-) allowed!
    </span>
</div>

然后添加样式,先用动画模拟上面提到的效果:

.name,.pwd{
    position: relative;
}
.callout{
    position: absolute;
    left: 178px;
    top:0;
    max-width: 20em;
    padding: .6em .8em;
    border-radius: .4em;
    background: #fb3;
    color: white;
    display: block;
    font: 80%/1 sans-serif;
    border: 1px solid rgba(0,0,0,.3);
    box-shadow: .05em .2em .6em rgba(0,0,0,.2);
}
.callout:before{
    content: "";
    position: absolute;
    top: .4em;
    left: -.5em;
    padding: .4em;
    background: inherit;
    border: inherit;
    border-top: none;
    border-right: none;
    transform: rotate(45deg);
    /*border: 1em solid transparent;
    border-right-color: #fb3;
    border-left-width: 0;*/
}
@keyframes elastic-grow{
    from {transform: scale(0);}
    70% {
        transform: scale(1.1);
        animation-timing-function: cubic-bezier(.1,.25,.1,.25);
    }
}
.uname-b:not(:focus) + .callout-b{
    transform: scale(0);
}
.uname-b:focus + .callout-b{
    animation: elastic-grow .5s;
}
.callout-b{
    transform-origin: 0% 10%;
}


虽然用动画可以模拟出刚开始的需求,但是有种大材小用的感觉,那么就换一个思路,利用上节所提到的过渡来增加弹性的效果。
这个思路主要还是利用cubic-bezier(),由上节知道x1,x2只能在区间[0-1]上取值。但是我们可以在垂直方向上突破0-1区间,从而让过渡达到低于0或者高于100%的程度。这就意味着在从scale(0)变形到scale(1)的过程中,可以在中间经历一个scale(1.1)。
在上面的代码中,要增加弹性效果,只需要把调速函数先达到110%,再过渡到100%,利用上节所提到的可视化工具,把第二个锚点向上移,跳到cubic-bezier(.25,.1,.3,1.5)的程度。

如上图,这个过渡会在50%的时间点到达100%,在70%达到110%,在最后回到100%。
我们为了便于对比,重新写一个demo
HTML:

<label>username:</label>
<div class="name">
    <input type="text" value="your name" class="uname-a">
    <span class="callout-a callout">
    只允许字母、数字、下划线(_)和连字符(-)
    </span>
</div>

CSS:

.uname-a:not(:focus) + .callout-a{
    transform: scale(0);
}
.callout-a{
    transition: .5s cubic-bezier(.25,.1,.3,1.5);
    transform-origin: 0% 10%;
}

仔细观察会发现,提示框在弹出的时候没问题,但是当失去焦点消失时,这个过程会将原来的110%变形(scale(1.1))解析为scale(-0.1),造成视觉上是从原始大小(scale(1))到消失(scale(-0.1))然后再稍微放大些,最后再消失(scale(0))。

要解决这个问题,只需要在失去焦点的时候,把调速函数覆盖为ease。

修改代码:

.uname-a:not(:focus) + .callout-a{
    transform: scale(0);
    transition-timing-function: ease;
}

但是稍微有些强迫症的人可能会发现,提示框关闭的动作要比弹出时慢一些。这是因为提示框弹出时在进行到50%(即250ms)就已经达到100%,而提示框的关闭过程需要500ms。这时候只需要显示的指定transition-duration即可。

.uname-a:not(:focus) + .callout-a{
    transform: scale(0);
    transition-timing-function: ease;
    transition-duration: .25s;
}

问题完美解决~